/* Mesquite source code.  Copyright 1997 and onward, D. Maddison and W. Maddison. 


Disclaimer:  The Mesquite source code is lengthy and we are few.  There are no doubt inefficiencies and goofs in this code. 
The commenting leaves much to be desired. Please approach this source code with the spirit of helping out.
Perhaps with your help we can be more than a few, and make Mesquite better.

Mesquite is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY.
Mesquite's web site is http://mesquiteproject.org

This source code and its compiled class files are free and modifiable under the terms of 
GNU Lesser General Public License.  (http://www.gnu.org/copyleft/lesser.html)
*/

package mesquite.categ.StateConsistencyStrip;

import java.awt.*;
import java.util.Enumeration;

import mesquite.lib.*;
import mesquite.lib.characters.*;
import mesquite.lib.duties.*;
import mesquite.lib.table.*;
import mesquite.categ.lib.*;



public class StateConsistencyStrip extends DataColumnNamesAssistant {
	long[] consensusSequence = null;
	double[] fractionMatchingConsensus = null;
	CategStateForCharacter stateTask = null;
	MesquiteString stateTaskName;
	MesquiteCommand stC;
	MesquiteSubmenuSpec stSubmenu;
	MesquiteMenuItemSpec menuItem1, menuItem2, closeMenuItem, lineMenuItem, windowSizesMenuItem, whiteThresholdItem;
	
	boolean suspend = false;

	MesquiteBoolean selectedOnly = new MesquiteBoolean(true);
	double whiteThreshold = 0.99;


	/*.................................................................................................................*/
	public boolean startJob(String arguments, Object condition, boolean hiredByName) {
		setUseMenubar(false);
		
		stateTask = (CategStateForCharacter)hireEmployee(CategStateForCharacter.class, "Calculator of character state for each character");
		if (stateTask == null)
			return sorry(getName() + " couldn't start because no calculator of character state obtained");
		stateTaskName = new MesquiteString(stateTask.getName());
		stC = makeCommand("setStateCalculator",  this);
		stateTask.setHiringCommand(stC);
		if (numModulesAvailable(CategStateForCharacter.class)>1) {
			MesquiteSubmenuSpec stSubmenu = addSubmenu(null, "State Calculator", stC, CategStateForCharacter.class);
			stSubmenu.setSelected(stateTaskName);
		}
		
		return true;
  	 }
	public boolean loadModule(){
		return false; // not ready
	}
	public void getEmployeeNeeds(){  //This gets called on startup to harvest information; override this and inside, call registerEmployeeNeed
		EmployeeNeed e2 = registerEmployeeNeed(CategStateForCharacter.class, getName() + " needs a module to provide a character state (e.g., the consensus state) for each character.",
		"The character state provider is chosen initially or using the State Calculator submenu");
	}
	/*.................................................................................................................*/
	/** Generated by an employee who quit.  The MesquiteModule should act accordingly. */
	public void employeeQuit(MesquiteModule employee) {
		if (employee == stateTask)  
			iQuit();
	}
	/*.................................................................................................................*/
	public void getSubfunctions(){
		String  explanationString = "(An Info Strip of a Categorical Matrix Window) Displays a consensus sequence for categorical data, as indicated by the two arrows in the figure below. <br> <img src=\"" + MesquiteFile.massageFilePathToURL(getPath() + "consensus.gif");
		explanationString += "\"><br>To create a consensus sequence, choose Matrix>Add Char Info Strip>Consensus Sequence.  To adjust options, use the drop-down menu that appears when you touch on the consensus sequence.<br>";
		registerSubfunction(new FunctionExplanation("Consensus Sequence", explanationString, null, null));
		super.getSubfunctions();
	}
	/*.................................................................................................................*/
	public void deleteMenuItems() {
		deleteMenuItem(stSubmenu);
		deleteMenuItem(menuItem1);
		deleteMenuItem(menuItem2);
		deleteMenuItem(windowSizesMenuItem);
		deleteMenuItem(whiteThresholdItem);
	}
	public void deleteRemoveMenuItem() {
		deleteMenuItem(lineMenuItem);
		deleteMenuItem(closeMenuItem);
	}
	public void addRemoveMenuItem() {
		closeMenuItem= addMenuItem(null,"Remove State Consistency Strip", makeCommand("remove", this));
		lineMenuItem = addMenuSeparator();
	}
		
	public void setTableAndData(MesquiteTable table, CharacterData data) {
		deleteMenuItems();
		deleteRemoveMenuItem();
		addRemoveMenuItem();
		stSubmenu = addSubmenu(null, "State Calculator", stC, CategStateForCharacter.class);
		stSubmenu.setSelected(stateTaskName);
		menuItem1= addCheckMenuItem(null,"Selected Taxa Only", makeCommand("toggleSelectedOnly", this), selectedOnly);
		whiteThresholdItem= addMenuItem(null,"White Threshold...", makeCommand("whiteThreshold", this));

		
		if (data != null)
			data.removeListener(this);
		this.data = data;
		this.table = table;
		data.addListener(this);
		
		calculateSequence();
	}
	/*.................................................................................................................*/
	public void dispose() {
		super.dispose();
		if (data!=null)
			data.removeListener(this);
	}
	/*.................................................................................................................*/
	 public String getShortParameters() {
		 String s = "(";
		 if (stateTask!=null)
			 s+= stateTask.getShortParameters();
		 s += ")";
		 return s;
	 }
	/*.................................................................................................................*/
	public Snapshot getSnapshot(MesquiteFile file) {
		Snapshot temp = new Snapshot();

		temp.addLine("suspend");
		temp.addLine("setStateCalculator " , stateTask);
		temp.addLine("toggleSelectedOnly " + selectedOnly.toOffOnString());
		temp.addLine("whiteThreshold " + MesquiteDouble.toString(whiteThreshold));

		temp.addLine("resume");

		return temp;
	}
	/*.................................................................................................................*/
	public Object doCommand(String commandName, String arguments, CommandChecker checker) {
		if (checker.compare(this.getClass(), "Sets whether or not only selected taxa are included are all taxa.", "[on or off]", commandName, "toggleSelectedOnly")) {
			boolean current = selectedOnly.getValue();
			selectedOnly.toggleValue(parser.getFirstToken(arguments));
			if (stateTask!=null)
				stateTask.setSelectedOnly(selectedOnly.getValue());
			if (current!=selectedOnly.getValue() && !suspend) {
				parametersChanged();
				calculateSequence();
				if (table !=null) {
						table.repaintAll();
				}
			}
		}
		else if (checker.compare(this.getClass(), "Sets the consistency score below which cells are colored white.", "[value]", commandName, "whiteThreshold")) {
			MesquiteInteger pos = new MesquiteInteger();
			pos.setValue(0);
			double value = MesquiteDouble.fromString(arguments, pos);
			if (MesquiteDouble.isCombinable(value)) {
				if (value>=0.0 && value<1.0 ) {
					whiteThreshold = value;
					parametersChanged();
					calculateSequence();
					if (table !=null) {
							table.repaintAll();
					}
				}
			}
			else { 
				double newValue = MesquiteDouble.queryDouble(containerOfModule(), "White Threshold", "White Threshold", whiteThreshold ,0.0, MesquiteDouble.infinite);
				if (MesquiteDouble.isCombinable(newValue) && newValue>=0.0 && newValue<1.0 && newValue!=whiteThreshold) {
					whiteThreshold = newValue;
					parametersChanged();
					calculateSequence();
					if (table !=null) {
							table.repaintAll();
					}
				}
			}
		}
		else if (checker.compare(this.getClass(), "Removes the Info Strip", null, commandName, "remove")) {
			iQuit();
		}
		else if (checker.compare(this.getClass(), "Suspends calculations", null, commandName, "suspend")) {
			suspend = true;
		}
		else if (checker.compare(this.getClass(), "Resumes calculations", null, commandName, "resume")) {
			suspend = false;
			calculateSequence();
			parametersChanged();
		}
		else if (checker.compare(this.getClass(), "Sets the calculator of a state for each character", "[name of state calculator module]", commandName, "setStateCalculator")) {
			CategStateForCharacter temp = (CategStateForCharacter)replaceEmployee(CategStateForCharacter.class, arguments, "Calculator of character state for each character", stateTask);
			if (temp !=null){
				stateTask = temp;
				stateTask.setHiringCommand(stC);
				stateTaskName.setValue(stateTask.getName());
				stateTask.setSelectedOnly(selectedOnly.getValue());
				if (!suspend) {
					parametersChanged();
					calculateSequence();
					if (table !=null){
						table.repaintAll();
					}
				}
				return stateTask;
			}
			addRemoveMenuItem();
		}
		else
			return  super.doCommand(commandName, arguments, checker);
		return null;
	}
	/*.................................................................................................................*/
	public boolean canHireMoreThanOnce(){
		return true;
	}
	/*.................................................................................................................*/
  	 public void employeeParametersChanged(MesquiteModule employee, MesquiteModule source, Notification notification) {
 		calculateSequence();
			if (table !=null)
				table.repaintAll();
  	 }
	/*.................................................................................................................*/
	/** Returns CompatibilityTest so other modules know if this is compatible with some object. */
	public CompatibilityTest getCompatibilityTest(){
		return new RequiresAnyCategoricalData();
	}
	/*.................................................................................................................*/
	public void endJob() {
		if (table!=null) {
			((ColumnNamesPanel)table.getColumnNamesPanel()).decrementInfoStrips();
			table.resetTableSize(false);
		}
		super.endJob();
	}

 
	/*.................................................................................................................*/
	 public boolean atLeastOneInapplicable(int ic){
		 if (data==null || table==null)
			 return false;
		 //long s;
		 int numTaxa = data.getNumTaxa();
		 int numTaxaWithData = 0;
		boolean noRowsSelected =  !table.anyRowSelected();
		 for (int it=0; it<numTaxa; it++) {
			 if (!selectedOnly.getValue() || table.isRowSelected(it) || noRowsSelected) {
				 long s= ((CategoricalData)data).getState(ic,it);
				 if (CategoricalState.isInapplicable(s))
					 return true;
			 }
		 }
		 return false;
	 }

		/*.................................................................................................................*/
	 public void calculateSequence() {
		 if (data == null)
			 return;
		 CategoricalState resultState = new CategoricalState();
		 MesquiteString resultString = new MesquiteString();
		 long[] sequence = new long[data.getNumChars()];
		 double[] fractionMatch = new double[data.getNumChars()];
		 MesquiteDouble fractionMatching = new MesquiteDouble();
		 for (int i = 0; i<data.getNumChars(); i++) {
			 stateTask.calculateState( (CategoricalData)data,  i,  table,  resultState,  resultString, fractionMatching);
			 fractionMatch[i] = fractionMatching.getValue();
			 sequence[i] = resultState.getValue();
		 }
		consensusSequence = sequence;
		fractionMatchingConsensus = fractionMatch;
	 }
		/*.................................................................................................................*/
	 public double scoreForCell (int ic, int numChars) {
		 if (fractionMatchingConsensus!=null) {
				double card = CategoricalState.cardinality(consensusSequence[ic]);
				if (card >=2) 
					return fractionMatchingConsensus[ic]/(card*1.0);
				else 
					return fractionMatchingConsensus[ic];
			 }
		return 0.0;
	 }

		/*.................................................................................................................*/
	 public void drawInCell(int ic, Graphics g, int x, int y, int w, int h, boolean selected) {
		 if (stateTask==null || data == null) 
			 return;
		 long s= CategoricalState.inapplicable;
		 if (consensusSequence==null)
			 calculateSequence();
		 if (ic>=0 && ic<consensusSequence.length)
			 s =consensusSequence[ic];

//		 boolean colorSomeInapp = atLeastOneInapplicable(ic) && showSomeInapplicableAsGray.getValue();
		 if (!CategoricalState.isEmpty(s) && !CategoricalState.isInapplicable(s)){
			 boolean grayText = false;
			 int e =  CategoricalState.getOnlyElement(s);
			 double score = scoreForCell(ic, data.getNumChars());
			 Color cellColor = MesquiteColorTable.getGrayScale(score,whiteThreshold,1.0);
			 g.setColor(cellColor);
			 g.fillRect(x,y,w,h);

			 if (grayText) {
					 g.setColor(Color.lightGray);
			 }
			 else {
					/* float[] hsb = new float[3];
					hsb[0]=hsb[1]=hsb[2]= 1;
					Color.RGBtoHSB(cellColor.getRed(), cellColor.getGreen(), cellColor.getBlue(), hsb);
					Color textColor = ColorDistribution.getContrasting(selected, cellColor, hsb, Color.white, Color.black);
					 */
					g.setColor(Color.black);
			 }
			 StringBuffer sb = new StringBuffer();
			 ((CategoricalData)data).statesIntoStringBufferCore(ic,  s,  sb, true,false, false);
			 FontMetrics fm = g.getFontMetrics(g.getFont());
			 int svp = StringUtil.getStringVertPosition(fm, y, h, null);

			 String cellString = sb.toString();
			 int length = fm.stringWidth(cellString);
			 int useX = x + (w - length) / 2;
			 g.drawString(cellString, useX, svp);

		 }
		 else if (CategoricalState.isInapplicable(s)){
			 g.setColor(Color.lightGray);
			 g.fillRect(x,y,w,h);
		}
		else {
			g.setColor(Color.white);
			g.fillRect(x,y,w,h);
		}
	}

	 /*.................................................................................................................*/
	 /** passes which object changed, along with optional integer (e.g. for character) (from MesquiteListener interface)*/
	 public void changed(Object caller, Object obj, Notification notification){
		 int code = Notification.getCode(notification);
		 if (obj instanceof Taxa &&  (Taxa)obj ==data.getTaxa()) {
			 if (code==MesquiteListener.SELECTION_CHANGED && selectedOnly.getValue()) {
				 calculateSequence();
			 }
			 else if (code==MesquiteListener.PARTS_ADDED || code==MesquiteListener.PARTS_DELETED) {
				 calculateSequence();
			 }
		 }
		 else if (obj instanceof CharacterData && (CharacterData)obj ==data) {
			 if (code==MesquiteListener.PARTS_DELETED || code==AssociableWithSpecs.SPECSSET_CHANGED || code==MesquiteListener.PARTS_ADDED || code==MesquiteListener.PARTS_MOVED || code==MesquiteListener.DATA_CHANGED) {
				 calculateSequence();
			 }
			 else{
				 calculateSequence();
			 }
		 }
		 super.changed(caller, obj, notification);
	 }
	 /*.................................................................................................................*/
	 /** returns whether this module is requesting to appear as a primary choice */
	 public boolean requestPrimaryChoice(){
		 return true;  
	 }
	 /*.................................................................................................................*/
	 public boolean isPrerelease(){
		 return true;  
	 }
	public String getTitle() {
		return "State Consistency Strip";
	}


	public String getName() {
		return "State Consistency Strip";
	}	
	
	
	public String getExplanation() {
		return "Displays overall consistency of states in each character as an info strip in a character matrix editor.";
	}
	/*.................................................................................................................*/
	/** returns the version number at which this module was first released.  If 0, then no version number is claimed.  If a POSITIVE integer
	 * then the number refers to the Mesquite version.  This should be used only by modules part of the core release of Mesquite.
	 * If a NEGATIVE integer, then the number refers to the local version of the package, e.g. a third party package*/
	public int getVersionOfFirstRelease(){
		return NEXTRELEASE;   //NOT YET RELEASED!!!
	}

}
